Deploy AWS Fargate Amazon ECS App and Infrastructure Using Terraform
Hello everyone, This is Aayush Jain in this Blog i will write about container Service and it's deployment using Terraform which is an IaC service
Introduction:
What is AWS Fargate?
AWS Fargate is a serverless compute engine for containers that allows you to run containerized applications without managing the underlying infrastructure.
https://aws.amazon.com/fargate/
What is Terraform?
Terraform is an open-source infrastructure as code (IaC) tool used to define, provision, and manage cloud resources across various providers with a declarative approach.
https://registry.terraform.io/providers/hashicorp/aws/latest/docs
Command Refferences:
Cmd | Discription |
---|---|
terraform init | This command initializes a Terraform project, downloading provider plugins and setting up the backend for managing infrastructure. |
terraform plan | This command generates an execution plan, showing a summary of the changes that Terraform will apply to the infrastructure without actually modifying it |
terraform apply | This command applies the changes specified in the Terraform configuration to provision or modify the infrastructure. |
terraform destroy | This command destroys all the resources defined in the Terraform configuration, effectively removing the provisioned infrastructure. |
What Is ECS?
Prerequisites:
- AWS Account
- IDE
- Terraform And terraform plugins
- Docker
- AWS CLI
for MacOS:
brew install terraform
for MacOS:
brew install Docker --cask
for MacOS:
brew install awscli
Architecture Diagram:
Parameters Used:
Networking
VPC
VPC Name | CIDR | Tenancy | Remarks |
---|---|---|---|
Production-VPC | 10.0.0.0/16 | default |
- Subnets
Subnet Name | Availability Zone | CIDR | Route Table | Remarks |
---|---|---|---|---|
Private-Subnet-1 | ap-northeast-1a | 10.0.3.0/24 | Private-Route-Table | Internet connection via Internet Gateway |
Private-Subnet-2 | ap-northeast-1c | 10.0.4.0/24 | Private-Route-Table | Internet connection via Internet Gateway |
Public-Subnet-1 | ap-northeast-1a | 10.0.1.0/24 | Public-Route-Table | Internet connection via NAT Gateway |
Public-Subnet-2 | ap-northeast-1c | 10.0.2.0/24 | Public-Route-Table | Internet connection via NAT Gateway |
- Internet Gateway
Item | Value | Remarks |
---|---|---|
Name | Production-IGW | |
Attached VPC | Production-VPC |
- NAT Gateway
Item | Value | Remarks |
---|---|---|
Name | Production-NAT-GW | |
Attached VPC | Production-VPC | |
Subnet | Public-Subnet-1 |
Route Tables
- Private-Route-Table
Item | Value | Remarks |
---|---|---|
Name | Private-Route-Table | |
VPC | Production-VPC | |
Subnet Association | Private-Subnet-1 Private-Subnet-2 |
- Routes for Private Route-Table
recipient | target | status | propagated | remarks |
---|---|---|---|---|
10.0.0.0/16 | local | active | no | |
0.0.0.0/0 | nat-xxx | active | no |
- Public-Route-Table
Item | Value | Remarks |
---|---|---|
Name | Public-Route-Table | |
VPC | Production-VPC | |
Subnet Association | Public-Subnet-1 Public-Subnet-2 |
- Routes for Public Route-Table
recipient | target | status | propagated | remarks |
---|---|---|---|---|
10.0.0.0/16 | local | active | no | |
0.0.0.0/0 | igw-xxx | active | no |
ALB
Item | Value | Remarks |
---|---|---|
Type | ALB | |
ELB Name | DevelopersIO-ECS-Cluster-ALB | |
Subnet | Public-Subnet-1 , Public-Subnet-2 | |
Security Group | DevelopersIO-ECS-Cluster-ALB-SG | |
Listener | HTTPS:443 | |
Deletion Protection | Disabled | |
Idle Timeout | 60 seconds | |
HTTP/2 | Enabled | |
Desync Mitigation Mode | Defensive | |
Drop Invalid Header Fields | Disabled | |
Access Logs | Disabled | |
Preserve host header | Disabled | |
Client port preservation | Disabled |
Listners
Path | Target | Security Policy | SSL Certificate | Notes |
---|---|---|---|---|
flask.xxx.xxxxxxx.xxx | flask-TG | ELBSecurityPolicy-TLS-1-2-2017-01 | *.Your_Domain |
ALB TargetGroup
Item | Configuration Value | Notes |
---|---|---|
Target Group Name | flask-TG | |
Port | http:50000 | |
Deregistration Delay | 300 seconds | |
Stickiness | Disabled | |
Targets | ip |
List of Security Groups
Security Group Name | VPC | Purpose | Notes |
---|---|---|---|
flask-SG | Production-VPC | ||
DevelopersIO-ECS-Cluster-SG | Production-VPC | ||
DevelopersIO-ECS-Cluster-ALB-SG | Production-VPC | ALB |
Security Group Configuration
flask-SG
Inbound Rules
Type | Protocol | Port Range | Source | Notes |
---|---|---|---|---|
Custom | TCP | 5000 | 10.0.0.0/16 | Within VPC |
HTTP | TCP | 80 | 10.0.0.0/16 | Within VPC |
- Outbound rules allow all traffic.
DevelopersIO-ECS-Cluster-ALB-SG
Inbound Rules
Type | Protocol | Port Range | Source | Notes |
---|---|---|---|---|
HTTPS | TCP | 443 | 0.0.0.0/0 | From Anywhere |
- Outbound rules allow all traffic.
IAM
IAM Role Name | IAM Policy |
---|---|
fargate_iam_role | fargate_iam_policy |
ECS
Cluster List
Cluster Name | CloudWatch monitoring | Notes |
---|---|---|
Production-Fargate-Cluster | Default |
Service List
Service Name | Cluster Name | Notes |
---|---|---|
flask | Production-Fargate-Cluster |
Service Configuration
Item | Value | Notes |
---|---|---|
Launch Type | FARGATE | |
Task Definition | flask | |
Service Type | REPLICA | |
Number of Tasks | 2 | |
Deployment Type | ECS | |
Minimum Health Percentage | 100 | |
Maximum Percentage | 200 | |
Circuit Breaker | Disabled | |
VPC | Production-VPC | |
Subnet | Private-Subnet-1,Private-Subnet-2 | |
Security Group | flask-SG | |
Public IP | Turned on | |
Health Check Grace Period | - | |
Load Balancer | DevelopersIO-ECS-Cluster-ALB | |
Target Group | flask-TG | |
Enable Service Discovery Integration | Disabled | |
AutoScaling | Do not adjust the desired count |
Task Definition
flask
Item | value | Notes |
---|---|---|
Compatibility Launch Type | FARGATE | |
Task Role | flask-IAM-Role | |
Network Mode | awsvpc | Fixed as awsvpc for FARGATE |
Task Execution Role | fargate-IAM-Role | |
Task Memory | 1024 | Potential for future expansion |
Task CPU | 512 | |
Container Definitions | flask | |
Service Integration | Disabled | |
Proxy Configuration | Disabled | |
Volumes | None |
Public ECR
Repository Name |
---|
flask-docker |
Route53 Records
Record name | Type | Routing policy | Route Traffic to |
---|---|---|---|
*.Your_Domain_Name | A | Simple | Alb Domain |
Prior to beginning, it is necessary to create an ECR repository and upload our Docker image to it.
Terraform Template:
VPC
resource "aws_ecs_cluster" "production-fargate-cluster" {
name = "Production-Fargate-Cluster"
}
resource "aws_alb" "ecs_cluster_alb" {
name = "${var.ecs_cluster_name}-ALB"
internal = false
security_groups = [aws_security_group.ecs_alb_security_group.id]
# subnets = [split(",", join(",", data.terraform_remote_state.infrastructure.outputs.public_subnets))]
subnets = tolist([aws_subnet.public-subnet-1.id, aws_subnet.public-subnet-2.id])
tags = {
Name = "${var.ecs_cluster_name}-ALB"
}
}
- The first code block defines an AWS ECS cluster resource with the name "Production-Fargate-Cluster".
- The second code block defines an AWS Application Load Balancer resource with the name "${var.ecs_cluster_name}-ALB". It is set to be an external load balancer (internal = false) and associated with the specified security groups and subnets.
resource "aws_alb_listener" "ecs_alb_https_listener" {
load_balancer_arn = aws_alb.ecs_cluster_alb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.ecs_domain_certificate.arn
default_action {
type = "forward"
target_group_arn = aws_alb_target_group.ecs_default_target_group.arn
}
depends_on = [aws_alb_target_group.ecs_default_target_group]
}
- This code block defines an AWS ALB listener for HTTPS traffic on port 443.
- It specifies the load balancer ARN, protocol, SSL policy, and certificate ARN for HTTPS communication.
- The default action for incoming requests is to forward them to the specified target group ARN.
- The code block depends on the creation of the target group before it can be successfully created.
resource "aws_alb_target_group" "ecs_default_target_group" {
name = "${var.ecs_cluster_name}-TG"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.production-vpc.id
tags = {
Name = "${var.ecs_cluster_name}-TG"
}
}
- This code block defines an AWS ALB target group for routing incoming requests to the ECS cluster.
- It specifies the target group name, port, protocol, and the VPC ID where the target group resides.
- Tags are added to the target group for identification purposes.
resource "aws_route53_record" "ecs_load_balancer_record" {
name = "*.${var.ecs_domain_name}"
type = "A"
zone_id = data.aws_route53_zone.ecs_domain.zone_id
alias {
evaluate_target_health = false
name = aws_alb.ecs_cluster_alb.dns_name
zone_id = aws_alb.ecs_cluster_alb.zone_id
}
}
- This code block defines an AWS Route 53 record for mapping a wildcard subdomain to the ALB's DNS name.
- It specifies the record name, type (A record), and the Route 53 zone ID where the record will be created.
- The alias is configured to point to the ALB's DNS name, and target health evaluation is
ALB.tf
Certainly! Here's the provided code with comments added to explain each code block:
resource "aws_ecs_cluster" "production-fargate-cluster" {
name = "Production-Fargate-Cluster"
}
- This code block defines an AWS ECS cluster resource with the name "Production-Fargate-Cluster".
resource "aws_alb" "ecs_cluster_alb" {
name = "${var.ecs_cluster_name}-ALB"
internal = false
security_groups = [aws_security_group.ecs_alb_security_group.id]
# subnets = [split(",", join(",", data.terraform_remote_state.infrastructure.outputs.public_subnets))]
subnets = tolist([aws_subnet.public-subnet-1.id, aws_subnet.public-subnet-2.id])
tags = {
Name = "${var.ecs_cluster_name}-ALB"
}
}
- This code block defines an AWS Application Load Balancer resource with the name "${var.ecs_cluster_name}-ALB".
- It is set to be an external load balancer (internal = false) and associated with the specified security groups and subnets.
resource "aws_alb_listener" "ecs_alb_https_listener" {
load_balancer_arn = aws_alb.ecs_cluster_alb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.ecs_domain_certificate.arn
default_action {
type = "forward"
target_group_arn = aws_alb_target_group.ecs_default_target_group.arn
}
depends_on = [aws_alb_target_group.ecs_default_target_group]
}
- This code block defines an AWS ALB listener for HTTPS traffic on port 443.
- It specifies the load balancer ARN, protocol, SSL policy, and certificate ARN for HTTPS communication.
- The default action for incoming requests is to forward them to the specified target group ARN.
- The code block depends on the creation of the target group before it can be successfully created.
resource "aws_alb_target_group" "ecs_default_target_group" {
name = "${var.ecs_cluster_name}-TG"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.production-vpc.id
tags = {
Name = "${var.ecs_cluster_name}-TG"
}
}
- This code block defines an AWS ALB target group for routing incoming requests to the ECS cluster.
- It specifies the target group name, port, protocol, and the VPC ID where the target group resides.
- Tags are added to the target group for identification purposes.
resource "aws_route53_record" "ecs_load_balancer_record" {
name = "*.${var.ecs_domain_name}"
type = "A"
zone_id = data.aws_route53_zone.ecs_domain.zone_id
alias {
evaluate_target_health = false
name = aws_alb.ecs_cluster_alb.dns_name
zone_id = aws_alb.ecs_cluster_alb.zone_id
}
}
- This code block defines an AWS Route 53 record for mapping a wildcard subdomain to the ALB's DNS name.
- It specifies the record name, type (A record), and the Route 53 zone ID where the record will be created.
- The alias is configured to point to the ALB's DNS name.
Security Group
variable "internet_cidr_block" {}
resource "aws_security_group" "ecs_security_group" {
name = "${var.ecs_cluster_name}-SG"
description = "Security group for ECS to allow inbound and outbound communication"
vpc_id = "${aws_vpc.production-vpc.id}"
# Ingress rules
ingress {
from_port = 32768
protocol = "TCP"
to_port = 65535
cidr_blocks = [var.internet_cidr_block]
}
ingress {
from_port = 5000
protocol = "TCP"
to_port = 5000
cidr_blocks = [var.internet_cidr_block]
}
# Egress rule to allow all outbound traffic
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = [var.internet_cidr_block]
}
tags = {
Name = "${var.ecs_cluster_name}-SG"
}
}
resource "aws_security_group" "ecs_alb_security_group" {
name = "${var.ecs_cluster_name}-ALB-SG"
description = "Security group for ALB to handle traffic for the ECS cluster"
vpc_id = "${aws_vpc.production-vpc.id}"
# Ingress rule for HTTPS traffic
ingress {
from_port = 443
protocol = "TCP"
to_port = 443
cidr_blocks = [var.internet_cidr_block]
}
# Egress rule to allow all outbound traffic
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = [var.internet_cidr_block]
}
}
taskdefination.tf
variable "ecs_service_name" {}
variable "docker_image_url" {}
variable "memory" {}
variable "docker_container_port" {}
variable "flask_profile" {}
variable "desired_task_number" {}
These lines define the variables that will be used in the Terraform configuration. These variables are placeholders that will be provided with values when executing the Terraform plan.
data "template_file" "ecs_task_definition_template" {
template = file("task_defination.json")
vars = {
task_definition_name = var.ecs_service_name
ecs_service_name = var.ecs_service_name
docker_image_url = var.docker_image_url
memory = var.memory
docker_container_port = var.docker_container_port
flask_profile = var.flask_profile
region = var.region
}
}
This block specifies a data source that reads a template file called "task_defination.json" and populates it with the variables defined above. The rendered template will be used later in the aws_ecs_task_definition
resource.
resource "aws_ecs_task_definition" "apache-task-definition" {
container_definitions = data.template_file.ecs_task_definition_template.rendered
family = var.ecs_service_name
cpu = 512
memory = var.memory
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
execution_role_arn = aws_iam_role.fargate_iam_role.arn
task_role_arn = aws_iam_role.fargate_iam_role.arn
}
This resource block defines an ECS task definition. It specifies the container definitions, task family, CPU and memory allocations, compatibility requirements, network mode, and the execution and task role ARNs.
resource "aws_iam_role" "fargate_iam_role" {
name = "${var.ecs_service_name}-IAM-Role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
This resource block creates an IAM role named ${var.ecs_service_name}-IAM-Role
which allows ECS services and tasks to assume this role.
resource "aws_iam_role_policy" "fargate_iam_policy" {
name = "${var.ecs_service_name}-IAM-Role"
role = aws_iam_role.fargate_iam_role.id
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:*",
"ecr:*",
"logs:*",
"cloudwatch:*",
"elasticloadbalancing:*"
],
"Resource": "*"
}
]
}
EOF
}
This resource block attaches a policy to the IAM role created above. The policy grants permissions for various ECS, ECR, CloudWatch, and Elastic Load Balancing actions.
resource "aws_ecs_service" "ecs_service" {
name = var.ecs_service_name
task_definition = var.ecs_service_name
desired_count = var.desired_task_number
cluster = aws_ecs_cluster.production-farg
ate-cluster.name
launch_type = "FARGATE"
network_configuration {
subnets = tolist([aws_subnet.private-subnet-1.id, aws_subnet.private-subnet-2.id])
security_groups = [aws_security_group.app_security_group.id]
assign_public_ip = true
}
load_balancer {
container_name = var.ecs_service_name
container_port = var.docker_container_port
target_group_arn = aws_alb_target_group.ecs_app_target_group.arn
}
}
This resource block creates an ECS service. It specifies the name, task definition, desired count, cluster, launch type, network configuration, and load balancer settings for the service.
resource "aws_security_group" "app_security_group" {
name = "${var.ecs_service_name}-SG"
description = "Security group for the flask app to communicate in and out"
vpc_id = aws_vpc.production-vpc.id
ingress {
from_port = 80
protocol = "TCP"
to_port = 80
cidr_blocks = [var.vpc_cidr]
}
ingress {
from_port = 5000
protocol = "TCP"
to_port = 5000
cidr_blocks = [var.vpc_cidr]
}
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.ecs_service_name}-SG"
}
}
This resource block creates a security group for the flask app. It allows inbound traffic on ports 80 and 5000 from the VPC CIDR range and allows all outbound traffic.
resource "aws_alb_target_group" "ecs_app_target_group" {
name = "${var.ecs_service_name}-TG"
port = var.docker_container_port
protocol = "HTTP"
vpc_id = aws_vpc.production-vpc.id
target_type = "ip"
health_check {
path = "/"
protocol = "HTTP"
matcher = "200"
interval = "60"
timeout = "30"
unhealthy_threshold = "3"
healthy_threshold = "3"
}
tags = {
Name = "${var.ecs_service_name}-TG"
}
}
This resource block creates a target group for the ECS application. It specifies the name, port, protocol, VPC ID, target type, and health check settings for the target group.
resource "aws_alb_listener_rule" "ecs_alb_listener_rule" {
listener_arn = aws_alb_listener.ecs_alb_https_listener.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_alb_target_group.ecs_app_target_group.arn
}
condition {
host_header {
values = ["${lower(var.ecs_service_name)}.${var.ecs_domain_name}"]
}
}
}
This resource block creates a listener rule for the ALB listener. It specifies the listener ARN, priority, action type, and target group ARN. It also includes a condition to match the host header of the incoming requests.
resource "aws_cloudwatch_log_group" "flaskapp_log_group" {
name = "${var.ecs_service_name}-LogGroup"
}
This resource block creates a CloudWatch log group for the ECS service. It specifies the name of the log group based on the ECS service name.
domain.tf
variable "ecs_cluster_name" {}
variable "ecs_domain_name" {}
resource "aws_acm_certificate" "ecs_domain_certificate" {
domain_name = "*.${var.ecs_domain_name}"
validation_method = "DNS"
tags = {
Name = "${var.ecs_cluster_name}-Certificate"
}
}
This code block defines a variable for the ECS cluster name and ECS domain name. It also creates an ACM certificate resource for the ECS domain. The certificate's domain name is set to "*.ecs_domain_name" to include all subdomains. DNS validation method is used to validate the certificate, and tags are added to identify the certificate.
data "aws_route53_zone" "ecs_domain" {
name = var.ecs_domain_name
private_zone = false
}
This code block retrieves information about the Route 53 hosted zone for the ECS domain. It uses the domain name variable to find the corresponding zone. The private_zone parameter is set to false to retrieve information for a public hosted zone.
resource "aws_route53_record" "cert_validation" {
for_each = {
for ecs in aws_acm_certificate.ecs_domain_certificate.domain_validation_options : ecs.domain_name => {
name = ecs.resource_record_name
record = ecs.resource_record_value
type = ecs.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.ecs_domain.zone_id
}
This code block creates Route 53 DNS records for certificate validation. It uses the for_each meta-argument to iterate over the domain validation options of the ACM certificate resource. Each iteration represents a DNS record to be created. The record's name, value, type, TTL, and zone ID are specified based on the validation options.
resource "aws_acm_certificate_validation" "ecs_domain_certificate_validation" {
certificate_arn = aws_acm_certificate.ecs_domain_certificate.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
This code block performs certificate validation by creating a certificate validation resource. It specifies the ARN of the ACM certificate to be validated and the FQDNs of the validation records created in Route 53. The certificate validation process will check if the DNS records are properly configured to prove domain ownership.
Run Book
terraform init -var-file="dev.tfvars"
terraform apply -var-file="dev.tfvars"
Result
open your domain in new browser you should be able to view Docker page
flask.your_domain.xxx
Conclusion:
Deploying a Fargate ECS application and its infrastructure using Terraform provides a reliable and efficient approach to managing containerised applications on AWS. By utilising Terraform's declarative syntax and infrastructure-as-code principles, the process becomes automated, scalable and repeatable. Through the various Terraform resources and configurations, including task definitions, security groups, IAM roles, load balancers and DNS validations, the entire lifecycle of the Fargate ECS application can be seamlessly managed and orchestrated. This approach enables developers and operations teams to easily deploy and manage their applications, ensuring consistent and reliable environments while taking advantage of AWS' powerful Fargate service for container orchestration.